package movee.service;

import com.alibaba.excel.EasyExcel;
import movee.domain.api.CountedResponseData;
import movee.domain.constants.BackupDeviceStatus;
import movee.domain.constants.BackupErrType;
import movee.domain.dao.BackupDeviceDao;
import movee.domain.dto.BackupDeviceDto;
import movee.domain.dto.BackupDeviceExportDto;
import movee.domain.exception.AppAssert;
import movee.domain.exception.AppException;
import movee.domain.exception.AppStatusCode;
import movee.domain.mapper.BackupDeviceMapper;
import movee.domain.query.DownloadFileQuery;
import movee.domain.query.BackupDeviceSearchQuery;
import movee.domain.query.BatchDownloadFileQuery;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.google.common.base.Stopwatch;
import lombok.extern.slf4j.Slf4j;
import movee.utils.crypto.ZipUtils;
import net.sf.cglib.beans.BeanCopier;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 服务
 *
 * @author movee
 */
@Service
@Slf4j
public class AppTaskService {

    private final BackupDeviceMapper deviceMapper;
    private final BeanCopier deviceDtoCopier = BeanCopier.create(BackupDeviceDao.class, BackupDeviceDto.class, false);

    @Autowired
    public AppTaskService(BackupDeviceMapper deviceMapper) {
        this.deviceMapper = deviceMapper;
    }

    /**
     * 查询
     *
     * @param query 查询条件
     * @return 设备备份列表
     */
    @Transactional(rollbackFor = {Exception.class, Error.class})
    public CountedResponseData<List<BackupDeviceDto>> getBackupDeviceList(BackupDeviceSearchQuery query) {
        // 参数检查
        checkBackupDeviceSearchQuery(query);
        // 预处理
        normalizeBackupDeviceSearchQuery(query);

        // 查询
        Stopwatch stopwatch = Stopwatch.createStarted();
        PageHelper.startPage(query.getPage(), query.getSize(), "id desc");
        List<BackupDeviceDao> daos = deviceMapper.findAll(query);
        Long totalCount = new PageInfo<>(daos).getTotal();

        log.info("getBackupDeviceList search elapse {} ms", stopwatch.elapsed(TimeUnit.MILLISECONDS));
        stopwatch.stop();

        // 转换
        List<BackupDeviceDto> dtos = daos.stream().map(this::toBackupDeviceDto).collect(Collectors.toList());

        return new CountedResponseData<>(totalCount, dtos);
    }

    /**
     * 上传文件
     * @param uploadFile uploadFile
     */
    public void uploadFile(MultipartFile uploadFile) {
        try {
            File localFile = new File(uploadFile.getName());
            FileUtils.writeByteArrayToFile(localFile, uploadFile.getBytes());
        } catch (Throwable t) {
            log.warn("upload file failed. exception info: {}", ExceptionUtils.getStackTrace(t));
        }
    }

    /**
     * 获取文件内容，文件内容存放在response body中，用于web前端下载文件
     *
     * @param query 查询条件
     * @return 文件内容
     */
    public ResponseEntity<Object> exportExcelFile(BackupDeviceSearchQuery query) {

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024 * 1024);
        try {
            checkBackupDeviceSearchQuery(query);
            normalizeBackupDeviceSearchQuery(query);

            List<BackupDeviceDao> daos = deviceMapper.findAll(query);

            // 4. 转换
            List<BackupDeviceDto> dtos = daos.stream().map(this::toBackupDeviceDto).collect(Collectors.toList());


            EasyExcel.write(outputStream, BackupDeviceExportDto.class).sheet().doWrite(dtos);

            HttpHeaders headers = new HttpHeaders();
            String fileName = URLEncoder.encode("backup_export.xlsx", "UTF-8");
            headers.add("Content-Disposition", String.format("attachment;filename=%s", fileName));
            headers.add("Access-Control-Expose-Headers", "Content-Disposition");
            headers.add("Cache-Control", "no-cache,no-store,must-revalidate");
            headers.add("Pragma", "no-cache");
            headers.add("Expires", "0");

            return ResponseEntity.ok()
                .headers(headers)
                .contentLength(outputStream.size())
                .contentType(
                    MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
                .body(outputStream.toByteArray());
        } catch (IOException e) {
            log.info("read content failed. except: {}", ExceptionUtils.getStackTrace(e));
            throw new AppException(AppStatusCode.RESOURCE_NOT_FOUND, "read content failed", e, "");
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                log.error("exportExcelFile close outputStream failed");
            }
        }
    }

    /**
     * 获取文件内容，文件内容存放在response body中，用于web前端下载文件
     *
     * @param query 查询条件
     * @return 文件内容
     */
    public ResponseEntity<byte[]> downloadSingleFile(DownloadFileQuery query) {

        try {
            File file = new File(query.getFilePath());

            HttpHeaders headers = new HttpHeaders();
            String fileName = URLEncoder.encode(file.getName(), "UTF-8");
            headers.add("Content-Disposition", String.format("attachment;filename=%s", fileName));
            headers.add("Access-Control-Expose-Headers", "Content-Disposition");
            headers.add("Cache-Control", "no-cache,no-store,must-revalidate");
            headers.add("Pragma", "no-cache");
            headers.add("Expires", "0");

            return ResponseEntity.ok()
                .headers(headers)
                .contentLength(file.length())
                .contentType(MediaType.parseMediaType("application/octet-stream"))
                .body(FileUtils.readFileToByteArray(file));

        } catch (IOException e) {
            log.info("read file error. except: {}", ExceptionUtils.getStackTrace(e));

            throw new AppException(AppStatusCode.RESOURCE_NOT_FOUND, "read file error", e, "");
        }
    }

    /**
     * 批量下载文件的方法
     * 获取文件内容，文件内容存放在response body中，用于web前端下载文件
     *
     * @param query 查询条件
     * @return 文件内容
     */
    public ResponseEntity<byte[]> batchDownloadFile(BatchDownloadFileQuery query) {

        try {
            List<File> files = new ArrayList<>();
            for (String fileName : query.getFileNames()) {
                files.add(new File(fileName));
            }

            byte[] zipBytes = ZipUtils.zipFiles("downloadfiles", files);

            HttpHeaders headers = new HttpHeaders();
            String fileName = URLEncoder.encode("downloadfiles.zip", "UTF-8");
            headers.add("Content-Disposition", String.format("attachment;filename=%s", fileName));
            headers.add("Access-Control-Expose-Headers", "Content-Disposition");
            headers.add("Cache-Control", "no-cache,no-store,must-revalidate");
            headers.add("Pragma", "no-cache");
            headers.add("Expires", "0");
            return ResponseEntity.ok()
                    .headers(headers)
                    .contentLength(zipBytes.length)
                    .contentType(MediaType.parseMediaType("application/zip"))
                    .body(zipBytes);


        } catch (IOException e) {
            log.info("read file error. except: {}", ExceptionUtils.getStackTrace(e));
            throw new AppException(AppStatusCode.RESOURCE_NOT_FOUND, "read file error", e, "");
        }
    }

    private void checkBackupDeviceSearchQuery(BackupDeviceSearchQuery query) {

        Long taskId = query.getTaskId();
        Timestamp startTime = query.getStartTime();
        Timestamp endTime = query.getEndTime();

        if (taskId == null && startTime == null) {
            throw new AppException(AppStatusCode.INVALID_QUERY_PARAM, "任务ID和开始时间不能同时为空");
        }

        if (startTime == null && endTime != null) {
            throw new AppException(AppStatusCode.INVALID_QUERY_PARAM, "如果开始时间为空，结束时间也必须为空");
        }

        if (startTime != null) {
            if (endTime == null) {
                endTime = Timestamp.valueOf(LocalDateTime.now());
            }

            if (startTime.after(endTime)) {
                throw new AppException(AppStatusCode.INVALID_QUERY_PARAM, "开始时间必须小于结束时间");
            }

            LocalDateTime startDateTime = startTime.toLocalDateTime();
            LocalDateTime endDateTime = endTime.toLocalDateTime();
            if (!endDateTime.minusDays(31L).isBefore(startDateTime)
                || Math.abs(endDateTime.getMonthValue() % 12 - startDateTime.getMonthValue() % 12) > 1) {
                throw new AppException(AppStatusCode.INVALID_QUERY_PARAM, "时间间隔不能大于一个月");
            }
        }
    }

    private void normalizeBackupDeviceSearchQuery(BackupDeviceSearchQuery query) {
        if (!CollectionUtils.isEmpty(query.getStatuses())) {
            List<String> statuses = new ArrayList<>();
            for (String s : query.getStatuses()) {
                BackupDeviceStatus status = BackupDeviceStatus.fromChineseName(s);
                if (status == null) {
                    status = BackupDeviceStatus.fromName(s);
                }
                AppAssert.notNull(status, AppStatusCode.INVALID_QUERY_PARAM, "不支持的status值");
                statuses.add(status.name());
            }

            query.setStatuses(statuses);
        }

        if (!CollectionUtils.isEmpty(query.getErrTypes())) {
            List<String> types = new ArrayList<>();
            for (String t : query.getErrTypes()) {
                BackupErrType type = BackupErrType.fromChineseName(t);
                if (type == null) {
                    type = BackupErrType.fromName(t);
                }
                AppAssert.notNull(type, AppStatusCode.INVALID_QUERY_PARAM, "不支持的errType值");
                types.add(type.name());
            }
            query.setErrTypes(types);
        }
    }

    private BackupDeviceDto toBackupDeviceDto(BackupDeviceDao dao) {
        BackupDeviceDto dto = new BackupDeviceDto();
        deviceDtoCopier.copy(dao, dto, null);

        // 修正部分值
        dto.setStatus(dao.getStatus() == null ? "" : dao.getStatus().getChineseName());
        dto.setErrType(dao.getErrType() == null ? "" : dao.getErrType().getChineseName());
        return dto;
    }
}
